<?php
/**
 * appRain v 0.1.x
 *
 * LICENSE
 *
 * This source file is subject to the MIT license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.opensource.org/licenses/mit-license.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@apprain.com so we can send you a copy immediately.
 *
 * @copyright  Copyright (c) 2010 appRain, Team. (http://www.apprain.com)
 * @license    http://www.opensource.org/licenses/mit-license.php MIT license
 *
 * HELP
 *
 * Official Website
 * http://www.apprain.com/
 *
 * Download Link
 * http://www.apprain.com/download
 *
 * Documents Link
 * http ://www.apprain.com/docs
 */

/**
 * Class Orginally Developed by : Alexandre Tedeschi (d), alexandrebr@gmail.com
 *
 * Updated by : Reazaul Karim, info@apprain.com
 */

/**
 *	1. Create ZIP by adding file manually
 *    	$zip  = App::Module('Zip')
                   ->setFileName('/var/www/works/other/wikipedia.zip')
                   ->addDir('wikipedia')
                   ->addFile('/var/www/works/other/wikipedia.php',  'wikipedia/wikipedia.php')
                   ->save(true);
 *
 * 2. Compress a full directory
        $zip  = App::Module('Zip')
                   ->setFileName('racipe.zip')
                   ->Compress('/var/www/works/other/racipe',true);
 *
 * 3. Unzip a full Zip file
        $zip  = App::Module('Zip')
                   ->setFileName('test.zip')
                   ->Extract('/var/www');*/
class appRain_Base_Modules_Zip  extends appRain_Base_Objects
{
	public $lastError;
	public $compressedList;
	public $centralDirList;
	public $endOfCentral = Array();

	private $fh;
	// Signatures
	private $zipSignature = "\x50\x4b\x03\x04";
	private $dirSignature = "\x50\x4b\x01\x02";
	private $dirSignatureE= "\x50\x4b\x05\x06";

	var $overwrite    = true;
	var $files_count  = 0;

	public function __construct(){}

	/* DO UNZIP SECTION
	   =========================================================================*/

	public function extract($path="")
	{
		$this->getList();
        $this->unzipAll($path);
	}

	private function archiveFileName()
	{
		if(!$this->getFileName()) die('UpZip file name not found');

		return $this->getFileName();
	}

	private function getList($stopOnFile=false)
	{
		if(sizeof($this->compressedList))
		{
			return $this->compressedList;
		}

		// Open file, and set file handler
		$fh = fopen($this->archiveFileName(), "r");
		$this->fh = &$fh;
		if(!$fh)
		{
			return false;
		}

		if(!$this->_loadFileListByEOF($fh, $stopOnFile))
		{
			if(!$this->_loadFileListBySignatures($fh, $stopOnFile))
			{
				return false;
			}
		}

		return $this->compressedList;
	}

	private function getExtraInfo($compressedFileName)
	{
		return
			isset($this->centralDirList[$compressedFileName])?
			$this->centralDirList[$compressedFileName]:
			false;
	}
	private function getZipInfo($detail=false)
	{
		return $detail?
			$this->endOfCentral[$detail]:
			$this->endOfCentral;
	}

	private function unzip($compressedFileName, $targetFileName=false, $applyChmod=0777)
	{
		if(!sizeof($this->compressedList))
		{
			$this->getList(false, $compressedFileName);
		}

		$fdetails = &$this->compressedList[$compressedFileName];
		if(!isset($this->compressedList[$compressedFileName]))
		{
			return false;
		}
		if(substr($compressedFileName, -1) == "/")
		{
			return false;
		}
		if(!$fdetails['uncompressed_size'])
		{
			return $targetFileName?
				file_put_contents($targetFileName, ""):
				"";
		}

		fseek($this->fh, $fdetails['contents-startOffset']);
		$toUncompress = fread($this->fh, $fdetails['compressed_size']);
		$ret = $this->uncompress(
				$toUncompress,
				$fdetails['compression_method'],
				$fdetails['uncompressed_size'],
				$targetFileName
			);
		unset($toUncompress);
		if($applyChmod && $targetFileName)
			chmod($targetFileName, 0777);

		return $ret;
	}

	private function unzipAll($targetDir=false, $baseDir="", $maintainStructure=true, $applyChmod=0777)
	{
		if($targetDir === false)
			$targetDir = dirname($_SERVER['SCRIPT_FILENAME'])."/";

		if(substr($targetDir, -1) != "/")
			$targetDir .= "/";

		$lista = $this->getList();
		if(sizeof($lista)) foreach($lista as $fileName=>$trash)
		{
			$dirname  = dirname($fileName);
			$outDN    = "$targetDir/$dirname";

			if(substr($dirname, 0, strlen($baseDir)) != $baseDir)
				continue;

			if(!is_dir($outDN) && $maintainStructure)
			{
				$str = "";
				$folders = explode("/", $dirname);
				foreach($folders as $folder)
				{
					$str = $str?"$str/$folder":$folder;
					if(!is_dir("$targetDir/$str"))
					{
						mkdir("$targetDir/$str");
						if($applyChmod)
							chmod("$targetDir/$str", $applyChmod);
					}
				}
			}
			if(substr($fileName, -1, 1) == "/")
				continue;

			$maintainStructure?
				$this->unzip($fileName, "$targetDir/$fileName", $applyChmod):
				$this->unzip($fileName, "$targetDir/".basename($fileName), $applyChmod);
		}
	}

	private function close()
	{
		if($this->fh)
			fclose($this->fh);
	}

	private  function __destroy()
	{
		$this->close();
	}

	private function uncompress(&$content, $mode, $uncompressedSize, $targetFileName=false)
	{
		switch($mode)
		{
			case 0:
				// Not compressed
				return $targetFileName?
					file_put_contents($targetFileName, $content):
					$content;
			case 1:
				return false;
			case 2:
			case 3:
			case 4:
			case 5:
				return false;
			case 6:
				return false;
			case 7:
				return false;
			case 8:
				// Deflate
				return $targetFileName?
					file_put_contents($targetFileName, gzinflate($content, $uncompressedSize)):
					gzinflate($content, $uncompressedSize);
			case 9:
				return false;
			case 10:
				return false;
           case 12:
               // Bzip2
               return $targetFileName?
                   file_put_contents($targetFileName, bzdecompress($content)):
                   bzdecompress($content);
			case 18:
				return false;
			default:
				return false;
		}
	}

	private function getLastError()
	{
		return $this->lastError;
	}

	private function _loadFileListByEOF(&$fh, $stopOnFile=false)
	{
		// Check if there's a valid Central Dir signature.
		// Let's consider a file comment smaller than 1024 characters...
		// Actually, it length can be 65536.. But we're not going to support it.

		for($x = 0; $x < 1024; $x++)
		{
			fseek($fh, -22-$x, SEEK_END);

			$signature = fread($fh, 4);
			if($signature == $this->dirSignatureE)
			{
				// If found EOF Central Dir
				$eodir['disk_number_this']   = unpack("v", fread($fh, 2)); // number of this disk
				$eodir['disk_number']        = unpack("v", fread($fh, 2)); // number of the disk with the start of the central directory
				$eodir['total_entries_this'] = unpack("v", fread($fh, 2)); // total number of entries in the central dir on this disk
				$eodir['total_entries']      = unpack("v", fread($fh, 2)); // total number of entries in
				$eodir['size_of_cd']         = unpack("V", fread($fh, 4)); // size of the central directory
				$eodir['offset_start_cd']    = unpack("V", fread($fh, 4)); // offset of start of central directory with respect to the starting disk number
				$zipFileCommentLenght        = unpack("v", fread($fh, 2)); // zipfile comment length
				$eodir['zipfile_comment']    = $zipFileCommentLenght[1]?fread($fh, $zipFileCommentLenght[1]):''; // zipfile comment
				$this->endOfCentral = Array(
					'disk_number_this'=>$eodir['disk_number_this'][1],
					'disk_number'=>$eodir['disk_number'][1],
					'total_entries_this'=>$eodir['total_entries_this'][1],
					'total_entries'=>$eodir['total_entries'][1],
					'size_of_cd'=>$eodir['size_of_cd'][1],
					'offset_start_cd'=>$eodir['offset_start_cd'][1],
					'zipfile_comment'=>$eodir['zipfile_comment'],
				);

				// Then, load file list
				fseek($fh, $this->endOfCentral['offset_start_cd']);
				$signature = fread($fh, 4);

				while($signature == $this->dirSignature)
				{
					$dir['version_madeby']      = unpack("v", fread($fh, 2)); // version made by
					$dir['version_needed']      = unpack("v", fread($fh, 2)); // version needed to extract
					$dir['general_bit_flag']    = unpack("v", fread($fh, 2)); // general purpose bit flag
					$dir['compression_method']  = unpack("v", fread($fh, 2)); // compression method
					$dir['lastmod_time']        = unpack("v", fread($fh, 2)); // last mod file time
					$dir['lastmod_date']        = unpack("v", fread($fh, 2)); // last mod file date
					$dir['crc-32']              = fread($fh, 4);              // crc-32
					$dir['compressed_size']     = unpack("V", fread($fh, 4)); // compressed size
					$dir['uncompressed_size']   = unpack("V", fread($fh, 4)); // uncompressed size
					$fileNameLength             = unpack("v", fread($fh, 2)); // filename length
					$extraFieldLength           = unpack("v", fread($fh, 2)); // extra field length
					$fileCommentLength          = unpack("v", fread($fh, 2)); // file comment length
					$dir['disk_number_start']   = unpack("v", fread($fh, 2)); // disk number start
					$dir['internal_attributes'] = unpack("v", fread($fh, 2)); // internal file attributes-byte1
					$dir['external_attributes1']= unpack("v", fread($fh, 2)); // external file attributes-byte2
					$dir['external_attributes2']= unpack("v", fread($fh, 2)); // external file attributes
					$dir['relative_offset']     = unpack("V", fread($fh, 4)); // relative offset of local header
					$dir['file_name']           = fread($fh, $fileNameLength[1]);                             // filename
					$dir['extra_field']         = $extraFieldLength[1] ?fread($fh, $extraFieldLength[1]) :''; // extra field
					$dir['file_comment']        = $fileCommentLength[1]?fread($fh, $fileCommentLength[1]):''; // file comment

					// Convert the date and time, from MS-DOS format to UNIX Timestamp
					$BINlastmod_date = str_pad(decbin($dir['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
					$BINlastmod_time = str_pad(decbin($dir['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
					$lastmod_dateY = bindec(substr($BINlastmod_date,  0, 7))+1980;
					$lastmod_dateM = bindec(substr($BINlastmod_date,  7, 4));
					$lastmod_dateD = bindec(substr($BINlastmod_date, 11, 5));
					$lastmod_timeH = bindec(substr($BINlastmod_time,   0, 5));
					$lastmod_timeM = bindec(substr($BINlastmod_time,   5, 6));
					$lastmod_timeS = bindec(substr($BINlastmod_time,  11, 5));

					// Some protection agains attacks...
					$dir['file_name']     = $this->_decodeFilename($dir['file_name']);
					if(!$dir['file_name'] = $this->_protect($dir['file_name']))
						continue;

					$this->centralDirList[$dir['file_name']] = Array(
						'version_madeby'=>$dir['version_madeby'][1],
						'version_needed'=>$dir['version_needed'][1],
						'general_bit_flag'=>str_pad(decbin($dir['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
						'compression_method'=>$dir['compression_method'][1],
						'lastmod_datetime'  =>mktime($lastmod_timeH, $lastmod_timeM, $lastmod_timeS, $lastmod_dateM, $lastmod_dateD, $lastmod_dateY),
						'crc-32'            =>str_pad(dechex(ord($dir['crc-32'][3])), 2, '0', STR_PAD_LEFT).
											  str_pad(dechex(ord($dir['crc-32'][2])), 2, '0', STR_PAD_LEFT).
											  str_pad(dechex(ord($dir['crc-32'][1])), 2, '0', STR_PAD_LEFT).
											  str_pad(dechex(ord($dir['crc-32'][0])), 2, '0', STR_PAD_LEFT),
						'compressed_size'=>$dir['compressed_size'][1],
						'uncompressed_size'=>$dir['uncompressed_size'][1],
						'disk_number_start'=>$dir['disk_number_start'][1],
						'internal_attributes'=>$dir['internal_attributes'][1],
						'external_attributes1'=>$dir['external_attributes1'][1],
						'external_attributes2'=>$dir['external_attributes2'][1],
						'relative_offset'=>$dir['relative_offset'][1],
						'file_name'=>$dir['file_name'],
						'extra_field'=>$dir['extra_field'],
						'file_comment'=>$dir['file_comment'],
					);
					$signature = fread($fh, 4);
				}

				// If loaded centralDirs, then try to identify the offsetPosition of the compressed data.
				if($this->centralDirList) foreach($this->centralDirList as $filename=>$details)
				{
					$i = $this->_getFileHeaderInformation($fh, $details['relative_offset']);
					$this->compressedList[$filename]['file_name']          = $filename;
					$this->compressedList[$filename]['compression_method'] = $details['compression_method'];
					$this->compressedList[$filename]['version_needed']     = $details['version_needed'];
					$this->compressedList[$filename]['lastmod_datetime']   = $details['lastmod_datetime'];
					$this->compressedList[$filename]['crc-32']             = $details['crc-32'];
					$this->compressedList[$filename]['compressed_size']    = $details['compressed_size'];
					$this->compressedList[$filename]['uncompressed_size']  = $details['uncompressed_size'];
					$this->compressedList[$filename]['lastmod_datetime']   = $details['lastmod_datetime'];
					$this->compressedList[$filename]['extra_field']        = $i['extra_field'];
					$this->compressedList[$filename]['contents-startOffset']=$i['contents-startOffset'];
					if(strtolower($stopOnFile) == strtolower($filename))
						break;
				}
				return true;
			}
		}
		return false;
	}

	private function _loadFileListBySignatures(&$fh, $stopOnFile=false)
	{
		fseek($fh, 0);

		$return = false;
		for(;;)
		{
			$details = $this->_getFileHeaderInformation($fh);
			if(!$details)
			{
				fseek($fh, 12 - 4, SEEK_CUR); // 12: Data descriptor - 4: Signature (that will be read again)
				$details = $this->_getFileHeaderInformation($fh);
			}
			if(!$details)
			{
				break;
			}
			$filename = $details['file_name'];
			$this->compressedList[$filename] = $details;
			$return = true;
			if(strtolower($stopOnFile) == strtolower($filename))
				break;
		}

		return $return;
	}

	private function _getFileHeaderInformation(&$fh, $startOffset=false)
	{
		if($startOffset !== false)
			fseek($fh, $startOffset);

		$signature = fread($fh, 4);
		if($signature == $this->zipSignature)
		{
			// Get information about the zipped file
			$file['version_needed']     = unpack("v", fread($fh, 2)); // version needed to extract
			$file['general_bit_flag']   = unpack("v", fread($fh, 2)); // general purpose bit flag
			$file['compression_method'] = unpack("v", fread($fh, 2)); // compression method
			$file['lastmod_time']       = unpack("v", fread($fh, 2)); // last mod file time
			$file['lastmod_date']       = unpack("v", fread($fh, 2)); // last mod file date
			$file['crc-32']             = fread($fh, 4);              // crc-32
			$file['compressed_size']    = unpack("V", fread($fh, 4)); // compressed size
			$file['uncompressed_size']  = unpack("V", fread($fh, 4)); // uncompressed size
			$fileNameLength             = unpack("v", fread($fh, 2)); // filename length
			$extraFieldLength           = unpack("v", fread($fh, 2)); // extra field length
			$file['file_name']          = fread($fh, $fileNameLength[1]); // filename
			$file['extra_field']        = $extraFieldLength[1]?fread($fh, $extraFieldLength[1]):''; // extra field
			$file['contents-startOffset']= ftell($fh);

			// Bypass the whole compressed contents, and look for the next file
			fseek($fh, $file['compressed_size'][1], SEEK_CUR);

			// Convert the date and time, from MS-DOS format to UNIX Timestamp
			$BINlastmod_date = str_pad(decbin($file['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
			$BINlastmod_time = str_pad(decbin($file['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
			$lastmod_dateY = bindec(substr($BINlastmod_date,  0, 7))+1980;
			$lastmod_dateM = bindec(substr($BINlastmod_date,  7, 4));
			$lastmod_dateD = bindec(substr($BINlastmod_date, 11, 5));
			$lastmod_timeH = bindec(substr($BINlastmod_time,   0, 5));
			$lastmod_timeM = bindec(substr($BINlastmod_time,   5, 6));
			$lastmod_timeS = bindec(substr($BINlastmod_time,  11, 5));

			// Some protection agains attacks...
			$file['file_name']     = $this->_decodeFilename($file['file_name']);
			if(!$file['file_name'] = $this->_protect($file['file_name']))
				return false;

			// Mount file table
			$i = Array(
				'file_name'         =>$file['file_name'],
				'compression_method'=>$file['compression_method'][1],
				'version_needed'    =>$file['version_needed'][1],
				'lastmod_datetime'  =>mktime($lastmod_timeH, $lastmod_timeM, $lastmod_timeS, $lastmod_dateM, $lastmod_dateD, $lastmod_dateY),
				'crc-32'            =>str_pad(dechex(ord($file['crc-32'][3])), 2, '0', STR_PAD_LEFT).
									  str_pad(dechex(ord($file['crc-32'][2])), 2, '0', STR_PAD_LEFT).
									  str_pad(dechex(ord($file['crc-32'][1])), 2, '0', STR_PAD_LEFT).
									  str_pad(dechex(ord($file['crc-32'][0])), 2, '0', STR_PAD_LEFT),
				'compressed_size'   =>$file['compressed_size'][1],
				'uncompressed_size' =>$file['uncompressed_size'][1],
				'extra_field'       =>$file['extra_field'],
				'general_bit_flag'  =>str_pad(decbin($file['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
				'contents-startOffset'=>$file['contents-startOffset']
			);
			return $i;
		}
		return false;
	}

	private function _decodeFilename($filename)
	{
		$from = "\xb7\xb5\xb6\xc7\x8e\x8f\x92\x80\xd4\x90\xd2\xd3\xde\xd6\xd7\xd8\xd1\xa5\xe3\xe0".
		        "\xe2\xe5\x99\x9d\xeb\xe9\xea\x9a\xed\xe8\xe1\x85\xa0\x83\xc6\x84\x86\x91\x87\x8a".
				"\x82\x88\x89\x8d\xa1\x8c\x8b\xd0\xa4\x95\xa2\x93\xe4\x94\x9b\x97\xa3\x96\xec\xe7".
				"\x98ï";
		$to   = "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûýþÿ´";

		return strtr($filename, $from, $to);
	}

	private function _protect($fullPath)
	{

		$fullPath = strtr($fullPath, ":*<>|\"\x0\\", "......./");
		while($fullPath[0] == "/")
			$fullPath = substr($fullPath, 1);

		if(substr($fullPath, -1) == "/")
		{
			$base     = '';
			$fullPath = substr($fullPath, 0, -1);
		}
		else
		{
			$base     = basename($fullPath);
			$fullPath = dirname($fullPath);
		}

		$parts   = explode("/", $fullPath);
		$lastIdx = false;
		foreach($parts as $idx=>$part)
		{
			if($part == ".")
				unset($parts[$idx]);
			elseif($part == "..")
			{
				unset($parts[$idx]);
				if($lastIdx !== false)
				{
					unset($parts[$lastIdx]);
				}
			}
			elseif($part === '')
			{
				unset($parts[$idx]);
			}
			else
			{
				$lastIdx = $idx;
			}
		}

		$fullPath = sizeof($parts)?implode("/", $parts)."/":"";
		return $fullPath.$base;
	}


	/* DO ZIP SECTION
	   =========================================================================*/
	public function Compress($path=null, $isAutoDownload=false)
	{
		if(!isset($path) || !file_exists($path)) return false;
		$this->setTmpRootPath($path);
		$this->createBundle($path);
		$this->Save($isAutoDownload);
	}

	public function createBundle($dir = null)
    {
		$listDir = array();
		if(!isset($dir)) return $listDir;

        if($handler = opendir($dir))
		{
            while (($sub = readdir($handler)) !== FALSE)
			{
                if ($sub != "." && $sub != ".." && $sub != "Thumb.db")
				{
                    if(is_file($dir. DS .$sub))
					{
						$this->addFile($dir. DS .$sub,str_replace($this->getTmpRootPath() . DS,"",$dir. DS . $sub));
                    }elseif(is_dir($dir. DS . $sub))
					{
						$this->addDir(str_replace($this->getTmpRootPath() . DS,"",$dir. DS . $sub));
                        $listDir[$sub] = $this->createBundle($dir."/".$sub);
                    }
                }
            }
            closedir($handler);
        }
        return $listDir;
    }


	public function addDir($dirname, $fileComments='')
	{
		if(substr($dirname, -1) != '/')
			$dirname .= '/';
		$this->addFile(false, $dirname, $fileComments);

		return $this;
	}

	public function addFile($filename, $cfilename, $fileComments='', $data=false)
	{
		if(!($fh = &$this->fh))
			$fh = fopen($this->archiveFileName(), $this->overwrite?'wb':'a+b');

		// $filename can be a local file OR the data wich will be compressed
		if(substr($cfilename, -1)=='/')
		{
			$details['uncsize'] = 0;
			$data = '';
		}
		elseif(file_exists($filename))
		{
			$details['uncsize'] = filesize($filename);
			$data = file_get_contents($filename);
		}
		elseif($filename)
		{
			echo "<b>Cannot add $filename. File not found</b><br>";
			return false;
		}
		else
		{
			$details['uncsize'] = strlen($filename);
			// DATA is given.. use it! :|
		}

		// if data to compress is too small, just store it
		if($details['uncsize'] < 256)
		{
			$details['comsize'] = $details['uncsize'];
			$details['vneeded'] = 10;
			$details['cmethod'] = 0;
			$zdata = &$data;
		}
		else
		{ // otherwise, compress it
			$zdata = gzcompress($data);
			$zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); // fix crc bug (thanks to Eric Mueller)
			$details['comsize'] = strlen($zdata);
			$details['vneeded'] = 10;
			$details['cmethod'] = 8;
		}

		$details['bitflag'] = 0;
		$details['crc_32']  = crc32($data);

		// Convert date and time to DOS Format, and set then
		$lastmod_timeS  = str_pad(decbin(date('s')>=32?date('s')-32:date('s')), 5, '0', STR_PAD_LEFT);
		$lastmod_timeM  = str_pad(decbin(date('i')), 6, '0', STR_PAD_LEFT);
		$lastmod_timeH  = str_pad(decbin(date('H')), 5, '0', STR_PAD_LEFT);
		$lastmod_dateD  = str_pad(decbin(date('d')), 5, '0', STR_PAD_LEFT);
		$lastmod_dateM  = str_pad(decbin(date('m')), 4, '0', STR_PAD_LEFT);
		$lastmod_dateY  = str_pad(decbin(date('Y')-1980), 7, '0', STR_PAD_LEFT);

		# echo "ModTime: $lastmod_timeS-$lastmod_timeM-$lastmod_timeH (".date("s H H").")\n";
		# echo "ModDate: $lastmod_dateD-$lastmod_dateM-$lastmod_dateY (".date("d m Y").")\n";
		$details['modtime'] = bindec("$lastmod_timeH$lastmod_timeM$lastmod_timeS");
		$details['moddate'] = bindec("$lastmod_dateY$lastmod_dateM$lastmod_dateD");

		$details['offset'] = ftell($fh);
		fwrite($fh, $this->zipSignature);
		fwrite($fh, pack('s', $details['vneeded'])); // version_needed
		fwrite($fh, pack('s', $details['bitflag'])); // general_bit_flag
		fwrite($fh, pack('s', $details['cmethod'])); // compression_method
		fwrite($fh, pack('s', $details['modtime'])); // lastmod_time
		fwrite($fh, pack('s', $details['moddate'])); // lastmod_date
		fwrite($fh, pack('V', $details['crc_32']));  // crc-32
		fwrite($fh, pack('I', $details['comsize'])); // compressed_size
		fwrite($fh, pack('I', $details['uncsize'])); // uncompressed_size
		fwrite($fh, pack('s', strlen($cfilename)));   // file_name_length
		fwrite($fh, pack('s', 0));  // extra_field_length
		fwrite($fh, $cfilename);    // file_name
		// ignoring extra_field
		fwrite($fh, $zdata);

		// Append it to central dir
		$details['external_attributes']  = (substr($cfilename, -1)=='/'&&!$zdata)?16:32; // Directory or file name
		$details['comments']             = $fileComments;
		$this->appendCentralDir($cfilename, $details);
		$this->files_count++;

		return $this;
	}

	public function setExtra($filename, $property, $value)
	{
		$this->centraldirs[$filename][$property] = $value;
	}

	public function save($autoDownload=false,$zipComments='')
	{
		if(!($fh = &$this->fh))
			$fh = fopen($this->archiveFileName(), $this->overwrite?'w':'a+');

		$cdrec = "";
		foreach($this->centraldirs as $filename=>$cd)
		{
			$cdrec .= $this->dirSignature;
			$cdrec .= "\x0\x0";                  // version made by
			$cdrec .= pack('v', $cd['vneeded']); // version needed to extract
			$cdrec .= "\x0\x0";                  // general bit flag
			$cdrec .= pack('v', $cd['cmethod']); // compression method
			$cdrec .= pack('v', $cd['modtime']); // lastmod time
			$cdrec .= pack('v', $cd['moddate']); // lastmod date
			$cdrec .= pack('V', $cd['crc_32']);  // crc32
			$cdrec .= pack('V', $cd['comsize']); // compressed filesize
			$cdrec .= pack('V', $cd['uncsize']); // uncompressed filesize
			$cdrec .= pack('v', strlen($filename)); // file comment length
			$cdrec .= pack('v', 0);                // extra field length
			$cdrec .= pack('v', strlen($cd['comments'])); // file comment length
			$cdrec .= pack('v', 0); // disk number start
			$cdrec .= pack('v', 0); // internal file attributes
			$cdrec .= pack('V', $cd['external_attributes']); // internal file attributes
			$cdrec .= pack('V', $cd['offset']); // relative offset of local header
			$cdrec .= $filename;
			$cdrec .= $cd['comments'];
		}
		$before_cd = ftell($fh);
		fwrite($fh, $cdrec);

		// end of central dir
		fwrite($fh, $this->dirSignatureE);
		fwrite($fh, pack('v', 0)); // number of this disk
		fwrite($fh, pack('v', 0)); // number of the disk with the start of the central directory
		fwrite($fh, pack('v', $this->files_count)); // total # of entries "on this disk"
		fwrite($fh, pack('v', $this->files_count)); // total # of entries overall
		fwrite($fh, pack('V', strlen($cdrec)));     // size of central dir
		fwrite($fh, pack('V', $before_cd));         // offset to start of central dir
		fwrite($fh, pack('v', strlen($zipComments))); // .zip file comment length
		fwrite($fh, $zipComments);

		fclose($fh);

		if($autoDownload)
		{
			App::Helper('Utility')->download($this->archiveFileName());
		}
	}

	private function appendCentralDir($filename,$properties)
	{
		$this->centraldirs[$filename] = $properties;
	}
}
// END ZIP CLASS
// info@apprain.com